/*
 * Copyright (c) 2011, Andrew Cole
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

package net.andycole.xmiparser.model;

import java.util.HashMap;
import java.util.ArrayList;

import org.apache.log4j.Logger;

import org.netbeans.api.mdr.MDRepository;
import org.netbeans.api.mdr.MDRManager;

import net.andycole.xmiparser.transfer.TransferClass;
import net.andycole.xmiparser.transfer.TransferOperation;
import net.andycole.xmiparser.transfer.TransferParameter;
import net.andycole.xmiparser.transfer.TransferAttribute;
import net.andycole.xmiparser.transfer.ASTypes;

import net.andycole.netbeans.uml.modelmanagement.Project;
import net.andycole.netbeans.uml.modelmanagement.UmlPackage;
import net.andycole.netbeans.uml.foundation.core.Attribute;
import net.andycole.netbeans.uml.foundation.core.Classifier;
import net.andycole.netbeans.uml.foundation.core.DataType;

import net.andycole.netbeans.uml.foundation.core.Element;
import net.andycole.netbeans.uml.foundation.core.Generalization;
import net.andycole.netbeans.uml.foundation.core.Implementation;
import net.andycole.netbeans.uml.foundation.core.Operation;
import net.andycole.netbeans.uml.foundation.core.Parameter;
import net.andycole.netbeans.uml.foundation.core.TaggedValue;

import net.andycole.netbeans.uml.foundation.datatypes.Multiplicity;
import net.andycole.netbeans.uml.foundation.datatypes.MultiplicityRange;
import net.andycole.netbeans.uml.foundation.datatypes.Expression;
import net.andycole.netbeans.uml.foundation.datatypes.VisibilityKind;
import net.andycole.netbeans.uml.foundation.datatypes.VisibilityKindEnum;
import net.andycole.netbeans.uml.foundation.datatypes.ParameterDirectionKindEnum;

/**
 * Provides the ability to add content to a UML metamodel that is compatible
 * with that of Netbeans (version 6.5).
 * @author Andrew Cole
 */
public class NBModelImpl implements IModel
{
   private static final String UML_MM = "NBUML";
   private static final String UML_INSTANCE = "MyNBUMLExtent";

   private Logger LOG = Logger.getLogger(NBModelImpl.class);

   private MDRepository repository;
   // assume import successful until told otherwise
   private boolean importFail = false;
   private net.andycole.netbeans.uml.UmlPackage factory;
   private Project modelPackage;

   private HashMap<String, Classifier> classCache;
   private HashMap<String, UmlPackage> packageCache;
   
   private ArrayList<DeferredAttributeHolder> deferredAttributes;
   private ArrayList<DeferredParameterHolder> deferredParameters;

   private boolean useMultiplicities = false;

   public NBModelImpl()
   {
      super();
      init();
   }

   @Override
   public void setUseMultiplicities(boolean value)
   {
      useMultiplicities = value;
   }

   private void init()
   {
      repository = MDRManager.getDefault().getDefaultRepository();
      classCache = new HashMap<String, Classifier>();
      packageCache = new HashMap<String, UmlPackage>();
      deferredAttributes = new ArrayList<DeferredAttributeHolder>();
      deferredParameters = new ArrayList<DeferredParameterHolder>();
      
      // start a transaction
      boolean fail = true;
      net.andycole.netbeans.uml.UmlPackage uml = null;
      repository.beginTrans(true);
      try {
         // create a new MOF extent
         RepositoryManager manager = new RepositoryManager(repository);
         manager.setMMDetails(UML_MM, "/netbeans_Diff.xml", "UML");
         uml = (net.andycole.netbeans.uml.UmlPackage) manager.createDatamodel(UML_INSTANCE);

         modelPackage = uml.getModelManagement().getProject().createProject();
         modelPackage.setName("AS3Model");
         modelPackage.setDefaultLanguage("Flex");
         factory = uml;
         createPrimitives();
         fail = false;
      } catch (Exception e) {
//         System.err.println("Fatal error reading XMI for UML extent.");
//         e.printStackTrace(System.err);
         LOG.error("Fatal error reading XMI for UML extent.", e);
      } finally {
         // commit/rollback the transaction
         repository.endTrans(fail);
      }
   }

   @Override
   public void startTransaction(boolean isWrite)
   {
      repository.beginTrans(isWrite);
   }

   @Override
   public void endTransaction()
   {
      // iterate over the model elements that do not yet reference an NBM
      // type. (Looking it up using the associated qualified class name)
      for (DeferredAttributeHolder holder : deferredAttributes)
      {
         Classifier type = getClassByName(holder.qualifiedClassName, false);
         Attribute element = holder.element;
         element.setType(type);
      }
      for (DeferredParameterHolder holder : deferredParameters)
      {
         Classifier type = getClassByName(holder.qualifiedClassName, false);
         Parameter element = holder.element;
         element.setType(type);
      }

      // commit/rollback the transaction
      repository.endTrans(importFail);

      //MDRManager.getDefault().shutdownAll();
   }

   @Override
   public void addClass(TransferClass newClass)
   {
      String pkgName = newClass.getPackagePath();
      addClassByName(pkgName, newClass);
   }

   private Classifier getClassByName(String  qualClassName,
                                     boolean isInterface)
   {
      Classifier cachedClass = classCache.get(qualClassName);

      if (cachedClass == null)
      {
         String className = qualClassName;
         String pkgName = "";
         int lastSepIndex = qualClassName.lastIndexOf(".");
         if (lastSepIndex > 0)
         {
            className = qualClassName.substring(lastSepIndex+1);
            pkgName = qualClassName.substring(0, lastSepIndex);
         }
         UmlPackage parentPkg = getParentPackage(pkgName);
         // create class ModelElement and set in cache
         if (isInterface)
            cachedClass = factory.getCore().getInterface().createInterface();
         else
            cachedClass = factory.getCore().getUmlClass().createUmlClass();
         parentPkg.getOwnedElement().add(cachedClass);
         cachedClass.setOwner(parentPkg);
         classCache.put(qualClassName, cachedClass);
         cachedClass.setName(className);
      }
      return cachedClass;
   }

   private void addClassByName(String pkgName,
                               TransferClass newClass)
   {
      String qualifiedClassName = newClass.getQualifiedClassName();
      Classifier cachedClass = classCache.get(qualifiedClassName);

      if (cachedClass == null)
      {
         // create/get parent packages
         // get parent package
         UmlPackage parentPkg = getParentPackage(pkgName);
         // create class ModelElement and set in cache
         if (newClass.isInterface())
            cachedClass = factory.getCore().getInterface().createInterface();
         else
            cachedClass = factory.getCore().getUmlClass().createUmlClass();
         parentPkg.getOwnedElement().add(cachedClass);
         cachedClass.setOwner(parentPkg);
         classCache.put(qualifiedClassName, cachedClass);
      }

      cachedClass.setName(newClass.getClassName());
      
      // set class attributes/operations etc.
      for (TransferOperation method : newClass.getMethods())
      {
         addMethodToClass(method, cachedClass);
      }

      for (TransferAttribute attr : newClass.getAttributes())
      {
         addAttributeToClass(attr, cachedClass);
      }

      setSuperclasses(newClass, cachedClass);

//      newClass.getDependencies();
//      newClass.getAssociations();

      if (newClass.getAnnotation() != null &&
          newClass.getAnnotation().length() > 0)
      {
         createAnnotation(cachedClass, newClass.getAnnotation());
      }
   }

   private void setSuperclasses(TransferClass newClass, Classifier cachedClass)
   {
      // extends (generalization)
      // get the parent class from the cache
      String baseClassName = newClass.getExtends();
      if (baseClassName != null && baseClassName.length() > 0)
      {
         Classifier baseClass = getClassByName(baseClassName,
                                               false);
         Generalization gens = factory.getCore().getGeneralization().createGeneralization();
         gens.setGeneral(baseClass);
         gens.setSpecific(cachedClass);
         //cachedClass.getOwnedElement().add(gens);
         cachedClass.getGeneralization().add(gens);
      }

      // realization (abstraction)
      // iterate over list of implemented interfaces
      for (String interfaceClassName : newClass.getImplementsArray())
      {
         Classifier inter = getClassByName(interfaceClassName,
                                           true);

         Implementation impl = factory.getCore().getImplementation().createImplementation();
         impl.getSupplier().add(inter); // interface is supplier
         impl.getClient().add(cachedClass);
         impl.setOwner(cachedClass.getOwner());
//         if (inter.getOwner() != null)
//            inter.getOwner().getOwnedElement().add(impl);

         //inter.getAppliedStereotype.add(stereo); //??
         cachedClass.getClientDependency().add(impl);
      }
   }

   private void addMethodToClass(TransferOperation tMethod, Classifier cachedClass)
   {
      Operation op = factory.getCore().getOperation().createOperation();
      op.setName(tMethod.getName());
      cachedClass.getOwnedElement().add(op);
      op.setOwner(cachedClass);

      for (TransferParameter tParam : tMethod.getParameters())
      {
         Parameter param = factory.getCore().getParameter().createParameter();
         param.setName(tParam.getName());
         //op.getParameter().add(param);
         deferredParameters.add(new DeferredParameterHolder(param, tParam.getType()));
         op.getOwnedElement().add(param);
         param.setOwner(op);
      }

      op.setVisibility(getVisibilityKind(tMethod.getAccessLevel()));
      //op.setOwnerScope(getScopeKind(tMethod.getIsStatic()));
      op.setStatic(tMethod.isStatic());

      if (tMethod.getAnnotation() != null &&
          tMethod.getAnnotation().length() > 0)
      {
         createAnnotation(op, tMethod.getAnnotation());
      }

      String resultTypeName = tMethod.getResultType();
      if (resultTypeName != null && resultTypeName.length() > 0)
      {
         Parameter outParam = factory.getCore().getParameter().createParameter();
         //op.getParameter().add(outParam);
         //Classifier paramClass = getClassByName(resultTypeName,
         //                                       false);
         deferredParameters.add(new DeferredParameterHolder(outParam, resultTypeName));
         //outParam.setType(paramClass);
         outParam.setDirection(ParameterDirectionKindEnum.PDK_RESULT);
         op.getOwnedElement().add(outParam);
         outParam.setOwner(op);
      }
      
   }

   private VisibilityKind getVisibilityKind(String visibility)
   {
      //VisibilityKindEnum.VK_PUBLIC;
      VisibilityKind retKind = VisibilityKindEnum.VK_PRIVATE;
      if ("private".equals(visibility))
         retKind = VisibilityKindEnum.VK_PRIVATE;
      else if ("protected".equals(visibility))
         retKind = VisibilityKindEnum.VK_PROTECTED;
      else if ("public".equals(visibility))
         retKind = VisibilityKindEnum.VK_PUBLIC;
      return retKind;
   }


   private void addAttributeToClass(TransferAttribute tAttribute,
                                    Classifier cachedClass)
   {
      // if the type is not a primitive type and multiplicities are used then
      // create associations using the attribute name
      Attribute att = factory.getCore().getAttribute().createAttribute();
      att.setName(tAttribute.getName());
      
      att.setOwner(cachedClass);
      cachedClass.getOwnedElement().add(att);
      //att.setFinal(tAttribute.isConst());
      //att.setFinal(tAttribute.isFinal());
      att.setVisibility(getVisibilityKind(tAttribute.getAccessLevel()));
      att.setStatic(tAttribute.isStatic());
      Expression defaultExp = factory.getDataTypes().getExpression().createExpression();
      att.setDefault(defaultExp);
      Multiplicity defaultMult = factory.getDataTypes().getMultiplicity().createMultiplicity();
      att.setMultiplicity(defaultMult);

      if (tAttribute.getMetaType() != null && useMultiplicities)
      {
         MultiplicityRange range = factory.getDataTypes().getMultiplicityRange().createMultiplicityRange();
         range.setLower(0);
         range.setUpper("*");
         defaultMult.getRange().add(range);
         deferredAttributes.add(new DeferredAttributeHolder(att, tAttribute.getMetaType()));
      }
      else
      {
         deferredAttributes.add(new DeferredAttributeHolder(att, tAttribute.getType()));
      }
      

      if (tAttribute.getAnnotation() != null &&
          tAttribute.getAnnotation().length() > 0)
      {
         createAnnotation(att, tAttribute.getAnnotation());
      }
 
   }

   private void createAnnotation(Element parent, String annotationString)
   {
      TaggedValue tvalue = factory.getCore().getTaggedValue().createTaggedValue();
      tvalue.getDataValue().add(getNBFormatComment(annotationString));
      tvalue.setName("documentation");
      parent.getOwnedElement().add(tvalue);
      tvalue.setOwner(parent);
   }

   private String getNBFormatComment(String rawComment)
   {
      String prefix = "<p style=\"margin-top: 0\">\n";
      String postfix = "\n</p>";
      return prefix + rawComment + postfix;
   }

   private void createPrimitives()
   {
      for (ASTypes type : ASTypes.values())
      {
         DataType dt = factory.getCore().getDataType().createDataType();
         String typeName = type.toString();
         dt.setName(typeName);
         modelPackage.getOwnedElement().add(dt);
         //primitivesCache.put(typeName, dt);
         dt.setOwner(modelPackage);
         classCache.put(typeName, dt);
      }

   }

   @Override
   public Object getBasePackage()
   {
      return factory;
   }

   private UmlPackage getParentPackage(String pkgPath)
   {
      UmlPackage currentPackage = modelPackage;
      String[] pkgList = null;
      if (pkgPath.length() > 0)
      {
         pkgList = pkgPath.split("\\.");
         //int counter = 0;
         for (String pkgName : pkgList)
         {
            UmlPackage parentPkg = currentPackage;
            currentPackage = packageCache.get(pkgName);
            if (currentPackage == null)
            {
               currentPackage = factory.getModelManagement().getUmlPackage().createUmlPackage();
               currentPackage.setName(pkgName);
               parentPkg.getOwnedElement().add(currentPackage);
               currentPackage.setOwner(parentPkg);
               packageCache.put(pkgName, currentPackage);
            }
         }
      }

      return currentPackage;
   }

   private class DeferredParameterHolder
   {
      public DeferredParameterHolder(Parameter element,
                                     String qualifiedClassName)
      {
         this.element = element;
         this.qualifiedClassName = qualifiedClassName;
      }
      
      public Parameter element;
      public String qualifiedClassName;
   }

   private class DeferredAttributeHolder
   {
      public DeferredAttributeHolder(Attribute element,
                                     String qualifiedClassName)
      {
         this.element = element;
         this.qualifiedClassName = qualifiedClassName;
      }
      public Attribute element;
      public String qualifiedClassName;
   }
}
